package com.chiyoyo.multipay.framework.core.client.impl;

import com.chiyoyo.multipay.framework.core.client.PayClient;
import com.chiyoyo.multipay.framework.core.client.PayClientConfig;
import com.chiyoyo.multipay.framework.core.client.dto.order.PayOrderNotifyReqDTO;
import com.chiyoyo.multipay.framework.core.client.dto.order.PayOrderRespDTO;
import com.chiyoyo.multipay.framework.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.chiyoyo.multipay.framework.core.client.dto.refund.PayRefundRespDTO;
import com.chiyoyo.multipay.framework.core.client.dto.refund.PayRefundUnifiedReqDTO;
import com.chiyoyo.multipay.framework.core.client.dto.transfer.PayTransferRespDTO;
import com.chiyoyo.multipay.framework.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import com.chiyoyo.multipay.framework.core.client.exception.PayException;
import com.chiyoyo.multipay.framework.core.enums.transfer.PayTransferTypeEnum;
import com.chiyoyo.multipay.framework.core.utils.JsonUtils;
import com.chiyoyo.multipay.framework.core.utils.ValidationUtils;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**
 * @author chil
 * 支付客户端的抽象类，提供模板方法，减少子类的冗余代码
 */
@Slf4j
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {

    /**
     * 渠道编码
     */
    private final String channelCode;
    /**
     * 支付配置
     */
    protected Config config;

    public AbstractPayClient(String channelCode, Config config) {
        this.channelCode = channelCode;
        this.config = config;
    }

    @Override
    public String getChannelCode() {
        return channelCode;
    }

    /**
     * 初始化
     */
    public final void init() {
        doInit();
        log.debug("[init][客户端({}) 初始化完成]", getChannelCode());
    }

    /**
     * 自定义初始化
     */
    protected abstract void doInit();

    /**
     * 刷新配置方法
     *
     * @param config 新的配置对象
     *  判断传入的新配置对象与当前配置对象是否相同，如果不同则记录日志并更新配置对象，
     * 然后调用初始化方法进行初始化操作。如果两个配置对象都为null，则视为没有变化，直接返回。
     */
    public final void refresh(Config config) {
        // 判断是否更新，同时处理null的情况
        if (this.config != null && this.config.equals(config)) {
            return;
        }
        if (this.config == null && config == null) {
            // 如果传入的config也是null，则没有变化，直接返回
            return;
        }
        log.info("[refresh][客户端({})发生变化，重新初始化]", getChannelCode());
        this.config = config;
        // 初始化
        this.init();
    }

    // ============ 支付相关 ============

    /**
     * 调用支付渠道进行统一下单操作
     *
     * @param reqDTO 下单请求信息，包含订单相关的参数
     * @return PayOrderRespDTO 支付订单响应信息，包含支付订单的结果
     */
    @Override
    public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
        ValidationUtils.validate(reqDTO);
        // 执行统一下单
        PayOrderRespDTO resp;
        try {
            resp = doUnifiedOrder(reqDTO);
        } catch (Throwable ex) {
            // 系统异常，则包装成 PayException 异常抛出
            log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
                    getChannelCode(), JsonUtils.toJsonStr(reqDTO), ex);
            throw buildPayException(ex);
        }
        return resp;
    }

    /**
     * 执行统一下单操作
     *
     * @param reqDTO 下单请求信息，包含订单相关的参数
     * @return PayOrderRespDTO 支付订单响应信息，包含支付订单的结果
     * @throws Throwable 抛出异常，表示在执行统一下单过程中发生了错误
     */
    protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable;

    /**
     * 解析订单通知的方法
     *
     * @param reqDTO 支付订单通知请求DTO
     * @return 解析后的订单响应信息
     * @throws IllegalArgumentException 如果参数params或body为null时抛出此异常
     * @throws PayException             如果解析订单通知过程中发生异常，将异常信息包装为PayException异常并抛出
     */
    @Override
    public final PayOrderRespDTO parseOrderNotify(PayOrderNotifyReqDTO reqDTO) {
        Map<String, String> params = reqDTO.getParams();
        String body = reqDTO.getBody();
        try {
            return doParseOrderNotify(reqDTO);
        } catch (Throwable ex) {
            log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
                    getChannelCode(), params, body, ex);
            throw buildPayException(ex);
        }
    }

    /**
     * 解析订单通知的抽象方法
     *
     * @param reqDTO 支付订单通知请求DTO
     * @return 解析后的订单响应信息
     * @throws Throwable 抛出异常，表示解析订单通知时发生了错误
     */
    protected abstract PayOrderRespDTO doParseOrderNotify(PayOrderNotifyReqDTO reqDTO) throws Throwable;

    /**
     * 获取订单信息
     *
     * @param outTradeNo 外部订单号
     * @return PayOrderRespDTO 支付订单响应信息
     * @throws PayException 包装后的系统异常，当查询支付单出现异常时抛出
     */
    @Override
    public final PayOrderRespDTO getOrder(String outTradeNo) {
        try {
            return doGetOrder(outTradeNo);
        } catch (Throwable ex) {
            // 系统异常，则包装成 PayException 异常抛出
            log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
                    getChannelCode(), outTradeNo, ex);
            throw buildPayException(ex);
        }
    }

    /**
     * 抽象方法，用于获取订单信息
     *
     * @param outTradeNo 外部订单号
     * @return PayOrderRespDTO 支付订单响应信息
     * @throws Throwable 抛出异常，表示在获取订单信息过程中发生错误
     */
    protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable;

    // ============ 退款相关 ============

    /**
     * 执行统一退款操作
     *
     * @param reqDTO 退款请求DTO，包含退款相关信息
     * @return PayRefundRespDTO 退款响应DTO，包含退款结果信息
     * @throws PayException 当执行退款过程中出现异常时，将异常包装为PayException并抛出
     */
    @Override
    public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
        ValidationUtils.validate(reqDTO);
        // 执行统一退款
        PayRefundRespDTO resp;
        try {
            resp = doUnifiedRefund(reqDTO);
        } catch (Throwable ex) {
            // 系统异常，则包装成 PayException 异常抛出
            log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
                    getChannelCode(), JsonUtils.toJsonStr(reqDTO), ex);
            throw buildPayException(ex);
        }
        return resp;
    }

    /**
     * 执行统一退款操作的抽象方法
     *
     * @param reqDTO 退款请求参数，包含退款相关信息
     * @return PayRefundRespDTO 退款响应信息，包含退款结果
     * @throws Throwable 如果在执行退款操作时发生异常，则抛出该异常
     */
    protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;

    /**
     * 解析退款通知的响应数据
     *
     * @param params 退款通知的参数集合
     * @param body   退款通知的消息体
     * @return PayRefundRespDTO 退款响应信息
     * @throws PayException 如果解析退款通知失败，则抛出PayException异常
     */
    @Override
    public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
        try {
            return doParseRefundNotify(params, body);
        } catch (Throwable ex) {
            log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
                    getChannelCode(), params, body, ex);
            throw buildPayException(ex);
        }
    }

    /**
     * 抽象方法，用于解析退款通知信息
     *
     * @param params 包含退款通知信息的参数集合
     * @param body   退款通知的消息体
     * @return PayRefundRespDTO 解析后的退款响应信息
     * @throws Throwable 如果在解析退款通知信息过程中发生异常，则抛出该异常
     */
    protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws Throwable;

    /**
     * 根据外部订单号和外部退款号获取退款信息
     *
     * @param outTradeNo  外部订单号
     * @param outRefundNo 外部退款号
     * @return PayRefundRespDTO 退款响应信息
     * @throws PayException 如果在查询退款单过程中发生异常，则抛出此异常
     */
    @Override
    public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
        try {
            return doGetRefund(outTradeNo, outRefundNo);
        } catch (Throwable ex) {
            log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
                    getChannelCode(), outTradeNo, outRefundNo, ex);
            throw buildPayException(ex);
        }
    }

    /**
     * 抽象方法：根据订单号和退款号获取退款信息
     *
     * @param outTradeNo  订单号
     * @param outRefundNo 退款号
     * @return PayRefundRespDTO 退款响应信息
     * @throws Throwable 抛出异常，当在获取退款信息过程中发生异常时抛出
     */
    protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws Throwable;

    // ============ 转账相关 ============

    /**
     * 统一的转账接口实现方法
     *
     * @param reqDTO 统一支付转账请求DTO对象
     * @return PayTransferRespDTO 支付转账响应DTO对象
     * @throws PayException 当转账请求验证失败或转账过程中发生异常时抛出
     */
    @Override
    public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
        validatePayTransferReqDTO(reqDTO);
        PayTransferRespDTO resp;
        try {
            resp = doUnifiedTransfer(reqDTO);
        } catch (Throwable ex) {
            // 系统异常，则包装成 PayException 异常抛出
            log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]",
                    getChannelCode(), JsonUtils.toJsonStr(reqDTO), ex);
            throw buildPayException(ex);
        }
        return resp;
    }

    /**
     * 验证支付转账请求DTO
     *
     * @param reqDTO 支付转账请求DTO对象
     * @throws PayException 当转账类型未实现或未开启时抛出异常
     */
    private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) {
        PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
        switch (transferType) {
            case ALIPAY_BALANCE: {
                ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class);
                break;
            }
            case WX_BALANCE: {
                ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class);
                break;
            }
            default: {
                throw new PayException("功能未实现/未开启");
            }
        }
    }

    /**
     * 根据订单号和转账类型获取转账单详情
     *
     * @param outTradeNo 外部订单号
     * @param type       转账类型
     * @return PayTransferRespDTO 转账单详情
     * @throws PayException 当查询转账单发生异常时抛出
     */
    @Override
    public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
        try {
            return doGetTransfer(outTradeNo, type);
        } catch (Throwable ex) {
            log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
                    getChannelCode(), outTradeNo, type, ex);
            throw buildPayException(ex);
        }
    }

    protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws Throwable;

    protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable;

    // ========== 各种工具方法 ==========

    /**
     * 构建支付异常对象
     *
     * @param ex 原始异常对象
     * @return PayException 支付异常对象
     * @throws PayException 如果原始异常不是PayException类型，则重新创建一个PayException异常并抛出
     */
    private PayException buildPayException(Throwable ex) {
        if (ex instanceof PayException) {
            return (PayException) ex;
        }
        throw new PayException(ex);
    }

}
